package com.googlecode.cswish.struts.hibernate;

import java.io.InputStream;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import javax.annotation.Resource;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Service;

import com.googlecode.cswish.Config;
import com.googlecode.cswish.model.ModelInfo;
import com.googlecode.cswish.struts.ContextHolder;
import com.googlecode.cswish.struts.spring.BeanIntrospector;
import com.googlecode.cswish.struts.spring.Executor;

/**
 * Root --> SiteClassLoader --> ModelClassLoader
 * 
 * @author Jerry.Feng
 *
 */
@Service
public class DynamicClassLoaderFactory {

	private static final Logger logger = Logger.getLogger(DynamicClassLoaderFactory.class);

	@Resource
	private Config config;
	
	private Map<String, ModelClassLoader> childClassLoaderMapping;
	private Map<String, SiteClassLoader> siteClassLoaderMapping;
	
	private URL[] urls;
	
	private ClassLoader parentLoader;
	
	private Map<String, Executor> eventCallbacks = new HashMap<String, Executor>();
	
	private class CacheRemoveExecutor implements Executor {
		
		private Map cache;
		
		public CacheRemoveExecutor(Map cache) {
			this.cache = cache;
		}
		
		@SuppressWarnings({ "unchecked", "rawtypes" })
		@Override
		public Object execute(Object parameters) {
			if (cache != null && cache.size() > 0) {
				Set<Entry> entrySet = cache.entrySet();
				boolean isList = entrySet.iterator().next().getValue() instanceof List;
				if (isList) {
					for (Entry entry : entrySet) {
						List list = (List) entry.getValue();
						if (list != null) {
							boolean foundDynamicVal = false;
							for (Object val: list) {
								if (val instanceof IDynamic) {
									foundDynamicVal = true;
								}
							}
							if (foundDynamicVal) {
								entry.setValue(null);
							}
						}
					}
				} else {
					for (Entry entry : entrySet) {
						if (entry.getValue() instanceof IDynamic) {
							cache.remove(entry.getKey());
						}
					}
				}
			}
			return null;
		}
	};
	
	public Class forName(String className) {
		ModelInfo modelInfo = BeanIntrospector.getInstance().getModelInfo(className);
		try {
			ClassLoader classLoader = getClassLoader(modelInfo.getType(), true);
			if (classLoader != null) {
				return classLoader.loadClass(modelInfo.getType());
			}
		} catch (ClassNotFoundException e) {
		}
		return null;
	}
	
	public Class defineClass(String className, byte[] b) {
		ModelClassLoader childClassLoader = getChildClassLoader(className, true);
		Class clazz = childClassLoader.defineClass(className, b);
		return clazz;
	}
	
	public Class defineClass(String className, InputStream input) {
		Class clazz = null;
		try {
			String data = IOUtils.toString(input);
			String encryptData = simpleDecrypt(data);
			byte[] classData = Base64.decodeBase64(encryptData);
			return defineClass(className, classData);
		} catch (Exception e) {
			logger.error("Failed to load class: " + className + " from input stream", e);
		}
		return clazz;
	}
	
	private String simpleDecrypt(String encrypted) {  
        if (encrypted == null)  
            return "";  
        if (encrypted.length() == 0)  
            return "";
  
        // NOTICE: it's same with LicenseEncrypt.simpleEncrypt
        String SEED = "0933912345463123732314329";
        int RADIX = 16;
        BigInteger bi_confuse = new BigInteger(SEED);
        try {  
            BigInteger bi_r1 = new BigInteger(encrypted, RADIX);  
            BigInteger bi_r0 = bi_r1.xor(bi_confuse);  
  
            return new String(bi_r0.toByteArray());  
        } catch (Exception e) {
            return "";  
        }  
    }
	
	// dynamic class loader to maintain the new added class
	private static class ModelClassLoader extends URLClassLoader implements DClassLoader {
		private DynamicClassLoaderFactory factory;
		private String modelName;

		public ModelClassLoader(String modelName, URL[] urls, DynamicClassLoaderFactory factory) {
			super(urls, null);
			this.modelName = modelName;
			this.factory = factory;
		}

		@Override
		public Class<?> findClass(String name) throws ClassNotFoundException {
			if (modelName.equals(name)) {
				return super.findClass(name);
			} else {
				return factory.findClass(name);
			}
		}
		
		public Class<?> defineClass(String name, byte[] b) {
			return super.defineClass(name, b, 0, b.length);
		}

		@Override
		public ClassLoader getDefaultClassLoader() {
			return factory.parentLoader;
		}
	}
	
	public ClassLoader getSiteClassLoader(String siteName) {
		SiteClassLoader classLoader = siteClassLoaderMapping.get(siteName);
		if (classLoader == null) {
			// waste an instance to reduce the lock time
			String sitePath = config.getRealSitePath(siteName) + "/classes/";
			classLoader = new SiteClassLoader(siteName, sitePath, this.getClass().getClassLoader());
			synchronized (siteClassLoaderMapping) {
				SiteClassLoader siteClassLoader = siteClassLoaderMapping.get(siteName);
				if (siteClassLoader == null) {
					siteClassLoaderMapping.put(siteName, classLoader);
				}
			}
		}
		return classLoader;
	}
	
	public ClassLoader bindSiteClassLoader() {
		ContextHolder contextHolder = ContextHolder.get();
		Thread thread = Thread.currentThread();
		ClassLoader oldClassLoader = thread.getContextClassLoader();
		
		if (!(oldClassLoader instanceof SiteClassLoader)) {
			ClassLoader siteClassLoader = getSiteClassLoader(contextHolder.getSiteName());
			thread.setContextClassLoader(siteClassLoader);
			contextHolder.setOldClassLoader(oldClassLoader);
		}
		return oldClassLoader;
	}
	
	public void unbindSiteClassLoader() {
		ClassLoader oldClassLoader = ContextHolder.get().getOldClassLoader();
		if (oldClassLoader != null) {
			Thread.currentThread().setContextClassLoader(oldClassLoader);
		}
	}
	
	public void unloadSiteClassLoader(String siteName) {
		siteClassLoaderMapping.remove(siteName);
	}
	
	public void reloadSiteClassLoader(String siteName) {
		String sitePath = config.getRealSitePath(siteName) + "/classes/";
		SiteClassLoader newClassLoader = new SiteClassLoader(siteName, sitePath, this.getClass().getClassLoader());
		ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
		synchronized (siteClassLoaderMapping) {
			ClassLoader removedClassLoader = siteClassLoaderMapping.remove(siteName);
			if (removedClassLoader == currentClassLoader) {
				Thread.currentThread().setContextClassLoader(newClassLoader);
			}
			siteClassLoaderMapping.put(siteName, newClassLoader);
		}
	}
	
	public DynamicClassLoaderFactory() {
		this.parentLoader  = Thread.currentThread().getContextClassLoader();
		this.urls = getURLs(Thread.currentThread().getContextClassLoader());
		this.childClassLoaderMapping = new ConcurrentHashMap<>();
		this.siteClassLoaderMapping = new ConcurrentHashMap<>();
	}
	
	public void unloadClass(String name) {
		String staticName = ModelInfo.getStaticClassName(name);
		
		boolean unload = false;
		for (int i = 0; i < 100; i++) {
			String dynamicName = ModelInfo.getDynamicClassName(staticName, i);
			ClassLoader loader = childClassLoaderMapping.remove(dynamicName);
			if (loader == null) {
				break;
			}
			
			unload = true;
			ClassLoader curLoader = Thread.currentThread().getContextClassLoader();
			// current loader is dynamic loader, switch it to static class loader
			if (curLoader == loader) {
				Thread.currentThread().setContextClassLoader(parentLoader);
			}
		}
		
		// remove the cached instance reference
		if (unload) {
			Executor executor = eventCallbacks.get(staticName);
			if (executor != null) {
				executor.execute(null);
			}
		}
	}

	public ClassLoader getClassLoader(String name, boolean autoCreation) {
		if (ModelInfo.isDynamicClass(name)) {
			return getChildClassLoader(name, autoCreation);
		} else {
			return getSiteClassLoader(ContextHolder.get().getSiteName());
		}
	}
	
	public ClassLoader getDynamicClassLoader(String name) {
		if (!ModelInfo.isDynamicClass(name)) {
			name = ModelInfo.getDynamicClassName(name, 0);
		}
		return getChildClassLoader(name, true);
	}
	
	private ModelClassLoader getChildClassLoader(String name, boolean autoCreation) {
		ModelClassLoader childClassLoader = childClassLoaderMapping.get(name);
		if (childClassLoader == null && autoCreation) {
			childClassLoader = new ModelClassLoader(name, urls, this);
			childClassLoaderMapping.put(name, childClassLoader);
			//childClassLoader.loadClass(name);
		}
		return childClassLoader;
	}
	
	public Class<?> findClass(String name) throws ClassNotFoundException {
		ModelClassLoader childClassLoader = childClassLoaderMapping.get(name);
		if (childClassLoader != null) {
			return childClassLoader.loadClass(name);
		}
		
		if (ModelInfo.isDynamicClass(name)) {
			throw new ClassNotFoundException(name);
		} else {
			return getSiteClassLoader(ContextHolder.get().getSiteName()).loadClass(name);
		}
	}
	
	private URL[] getURLs(ClassLoader classLoader) {
		Map<String, URL> paths = new LinkedHashMap<String, URL>();
		do {
			if (classLoader instanceof URLClassLoader) {
				URLClassLoader urlLoader = (URLClassLoader) classLoader;
			
				URL[] sources = urlLoader.getURLs();
				for (URL source : sources) {
					if (!paths.containsKey(source.toString())) {
						paths.put(source.toString(), source);
					}
				}
			}
			classLoader = classLoader.getParent();
		} while (classLoader != null);
		URL[] urls = new URL[paths.size()];
		paths.values().toArray(urls);
		return urls;
	}

	public void registerUnloadEvent(String name, Executor executor) {
		eventCallbacks.put(name, executor);
	}
	
	public void registerUnloadCacheEvent(String name, Map cache) {
		eventCallbacks.put(name, new CacheRemoveExecutor(cache));
	}
}